{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "import torch.utils.data as utils\n",
    "import torch.nn.functional as F\n",
    "import torch\n",
    "import torch.nn as nn\n",
    "from torch.autograd import Variable\n",
    "from torch.nn.parameter import Parameter\n",
    "import numpy as np\n",
    "import pandas as pd\n",
    "import math\n",
    "import time"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "import matplotlib.pyplot as plt\n",
    "%matplotlib inline  "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "0.4.1\n"
     ]
    }
   ],
   "source": [
    "print(torch.__version__)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "def PrepareDataset(speed_matrix, BATCH_SIZE = 40, seq_len = 10, pred_len = 1, train_propotion = 0.7, valid_propotion = 0.2):\n",
    "    \"\"\" Prepare training and testing datasets and dataloaders.\n",
    "    \n",
    "    Convert speed/volume/occupancy matrix to training and testing dataset. \n",
    "    The vertical axis of speed_matrix is the time axis and the horizontal axis \n",
    "    is the spatial axis.\n",
    "    \n",
    "    Args:\n",
    "        speed_matrix: a Matrix containing spatial-temporal speed data for a network\n",
    "        seq_len: length of input sequence\n",
    "        pred_len: length of predicted sequence\n",
    "    Returns:\n",
    "        Training dataloader\n",
    "        Testing dataloader\n",
    "    \"\"\"\n",
    "    time_len = speed_matrix.shape[0]\n",
    "    \n",
    "    max_speed = speed_matrix.max().max()\n",
    "    speed_matrix =  speed_matrix / max_speed\n",
    "    \n",
    "    speed_sequences, speed_labels = [], []\n",
    "    for i in range(time_len - seq_len - pred_len):\n",
    "        speed_sequences.append(speed_matrix.iloc[i:i+seq_len].values)\n",
    "        speed_labels.append(speed_matrix.iloc[i+seq_len:i+seq_len+pred_len].values)\n",
    "    speed_sequences, speed_labels = np.asarray(speed_sequences), np.asarray(speed_labels)\n",
    "    \n",
    "    # shuffle and split the dataset to training and testing datasets\n",
    "    sample_size = speed_sequences.shape[0]\n",
    "    index = np.arange(sample_size, dtype = int)\n",
    "    np.random.shuffle(index)\n",
    "    \n",
    "    train_index = int(np.floor(sample_size * train_propotion))\n",
    "    valid_index = int(np.floor(sample_size * ( train_propotion + valid_propotion)))\n",
    "    \n",
    "    train_data, train_label = speed_sequences[:train_index], speed_labels[:train_index]\n",
    "    valid_data, valid_label = speed_sequences[train_index:valid_index], speed_labels[train_index:valid_index]\n",
    "    test_data, test_label = speed_sequences[valid_index:], speed_labels[valid_index:]\n",
    "    \n",
    "    train_data, train_label = torch.Tensor(train_data), torch.Tensor(train_label)\n",
    "    valid_data, valid_label = torch.Tensor(valid_data), torch.Tensor(valid_label)\n",
    "    test_data, test_label = torch.Tensor(test_data), torch.Tensor(test_label)\n",
    "    \n",
    "    train_dataset = utils.TensorDataset(train_data, train_label)\n",
    "    valid_dataset = utils.TensorDataset(valid_data, valid_label)\n",
    "    test_dataset = utils.TensorDataset(test_data, test_label)\n",
    "    \n",
    "    train_dataloader = utils.DataLoader(train_dataset, batch_size = BATCH_SIZE, shuffle=True, drop_last = True)\n",
    "    valid_dataloader = utils.DataLoader(valid_dataset, batch_size = BATCH_SIZE, shuffle=True, drop_last = True)\n",
    "    test_dataloader = utils.DataLoader(test_dataset, batch_size = BATCH_SIZE, shuffle=True, drop_last = True)\n",
    "    \n",
    "    return train_dataloader, valid_dataloader, test_dataloader, max_speed"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "if __name__ == \"__main__\":\n",
    "#     data = 'inrix'\n",
    "    data = 'loop'\n",
    "    directory = '../../Data_Warehouse/Data_network_traffic/'\n",
    "    if data == 'inrix':\n",
    "        speed_matrix =  pd.read_pickle( directory + 'inrix_seattle_speed_matrix_2012')\n",
    "        A = np.load(directory + 'INRIX_Seattle_2012_A.npy')\n",
    "        FFR_5min = np.load(directory + 'INRIX_Seattle_2012_reachability_free_flow_5min.npy')\n",
    "        FFR_10min = np.load(directory + 'INRIX_Seattle_2012_reachability_free_flow_10min.npy')\n",
    "        FFR_15min = np.load(directory + 'INRIX_Seattle_2012_reachability_free_flow_15min.npy')\n",
    "        FFR_20min = np.load(directory + 'INRIX_Seattle_2012_reachability_free_flow_20min.npy')\n",
    "        FFR_25min = np.load(directory + 'INRIX_Seattle_2012_reachability_free_flow_25min.npy')\n",
    "        FFR = [FFR_5min, FFR_10min, FFR_15min, FFR_20min, FFR_25min]\n",
    "    elif data == 'loop':\n",
    "        speed_matrix =  pd.read_pickle( directory + 'speed_matrix_2015')\n",
    "        A = np.load( directory + 'Loop_Seattle_2015_A.npy')\n",
    "        FFR_5min = np.load( directory + 'Loop_Seattle_2015_reachability_free_flow_5min.npy')\n",
    "        FFR_10min = np.load( directory + 'Loop_Seattle_2015_reachability_free_flow_10min.npy')\n",
    "        FFR_15min = np.load( directory + 'Loop_Seattle_2015_reachability_free_flow_15min.npy')\n",
    "        FFR_20min = np.load( directory + 'Loop_Seattle_2015_reachability_free_flow_20min.npy')\n",
    "        FFR_25min = np.load( directory + 'Loop_Seattle_2015_reachability_free_flow_25min.npy')\n",
    "        FFR = [FFR_5min, FFR_10min, FFR_15min, FFR_20min, FFR_25min]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "train_dataloader, valid_dataloader, test_dataloader, max_speed = PrepareDataset(speed_matrix)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "inputs, labels = next(iter(train_dataloader))\n",
    "[batch_size, step_size, fea_size] = inputs.size()\n",
    "input_dim = fea_size\n",
    "hidden_dim = fea_size\n",
    "output_dim = fea_size"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "def TrainModel(model, train_dataloader, valid_dataloader, learning_rate = 1e-5, num_epochs = 300, patience = 10, min_delta = 0.00001):\n",
    "    \n",
    "    inputs, labels = next(iter(train_dataloader))\n",
    "    [batch_size, step_size, fea_size] = inputs.size()\n",
    "    input_dim = fea_size\n",
    "    hidden_dim = fea_size\n",
    "    output_dim = fea_size\n",
    "    \n",
    "    model.cuda()\n",
    "    \n",
    "    loss_MSE = torch.nn.MSELoss()\n",
    "    loss_L1 = torch.nn.L1Loss()\n",
    "\n",
    "    learning_rate = 1e-5\n",
    "    optimizer = torch.optim.RMSprop(model.parameters(), lr = learning_rate)\n",
    "    \n",
    "    use_gpu = torch.cuda.is_available()\n",
    "    \n",
    "    interval = 100\n",
    "    losses_train = []\n",
    "    losses_valid = []\n",
    "    losses_epochs_train = []\n",
    "    losses_epochs_valid = []\n",
    "    \n",
    "    cur_time = time.time()\n",
    "    pre_time = time.time()\n",
    "    \n",
    "    # Variables for Early Stopping\n",
    "    is_best_model = 0\n",
    "    patient_epoch = 0\n",
    "    \n",
    "    for epoch in range(num_epochs):\n",
    "#         print('Epoch {}/{}'.format(epoch, num_epochs - 1))\n",
    "#         print('-' * 10)\n",
    "        \n",
    "        trained_number = 0\n",
    "        \n",
    "        valid_dataloader_iter = iter(valid_dataloader)\n",
    "        \n",
    "        losses_epoch_train = []\n",
    "        losses_epoch_valid = []\n",
    "\n",
    "        for data in train_dataloader:\n",
    "            inputs, labels = data\n",
    "\n",
    "            if inputs.shape[0] != batch_size:\n",
    "                continue\n",
    "\n",
    "            if use_gpu:\n",
    "                inputs, labels = Variable(inputs.cuda()), Variable(labels.cuda())\n",
    "            else: \n",
    "                inputs, labels = Variable(inputs), Variable(labels)\n",
    "                \n",
    "            model.zero_grad()\n",
    "\n",
    "            outputs = model(inputs)\n",
    "            \n",
    "            loss_train = loss_MSE(outputs, torch.squeeze(labels))\n",
    "            \n",
    "            losses_train.append(loss_train.data)\n",
    "            losses_epoch_train.append(loss_train.data)\n",
    "            \n",
    "            optimizer.zero_grad()\n",
    "            \n",
    "            loss_train.backward()\n",
    "            \n",
    "            optimizer.step()\n",
    "            \n",
    "            # validation \n",
    "            try: \n",
    "                inputs_val, labels_val = next(valid_dataloader_iter)\n",
    "            except StopIteration:\n",
    "                valid_dataloader_iter = iter(valid_dataloader)\n",
    "                inputs_val, labels_val = next(valid_dataloader_iter)\n",
    "            \n",
    "            if use_gpu:\n",
    "                inputs_val, labels_val = Variable(inputs_val.cuda()), Variable(labels_val.cuda())\n",
    "            else: \n",
    "                inputs_val, labels_val = Variable(inputs_val), Variable(labels_val)\n",
    "\n",
    "            outputs_val= model(inputs_val)\n",
    "\n",
    "            loss_valid = loss_MSE(outputs_val, torch.squeeze(labels_val))\n",
    "            losses_valid.append(loss_valid.data)\n",
    "            losses_epoch_valid.append(loss_valid.data)\n",
    "            \n",
    "            # output\n",
    "            trained_number += 1\n",
    "            \n",
    "        avg_losses_epoch_train = sum(losses_epoch_train) / float(len(losses_epoch_train))\n",
    "        avg_losses_epoch_valid = sum(losses_epoch_valid) / float(len(losses_epoch_valid))\n",
    "        losses_epochs_train.append(avg_losses_epoch_train)\n",
    "        losses_epochs_valid.append(avg_losses_epoch_valid)\n",
    "        \n",
    "        # Early Stopping\n",
    "        if epoch == 0:\n",
    "            is_best_model = 1\n",
    "            best_model = model\n",
    "            min_loss_epoch_valid = 10000.0\n",
    "            if avg_losses_epoch_valid < min_loss_epoch_valid:\n",
    "                min_loss_epoch_valid = avg_losses_epoch_valid\n",
    "        else:\n",
    "            if min_loss_epoch_valid - avg_losses_epoch_valid > min_delta:\n",
    "                is_best_model = 1\n",
    "                best_model = model\n",
    "                min_loss_epoch_valid = avg_losses_epoch_valid \n",
    "                patient_epoch = 0\n",
    "            else:\n",
    "                is_best_model = 0\n",
    "                patient_epoch += 1\n",
    "                if patient_epoch >= patience:\n",
    "                    print('Early Stopped at Epoch:', epoch)\n",
    "                    break\n",
    "        \n",
    "        # Print training parameters\n",
    "        cur_time = time.time()\n",
    "        print('Epoch: {}, train_loss: {}, valid_loss: {}, time: {}, best model: {}'.format( \\\n",
    "                    epoch, \\\n",
    "                    np.around(avg_losses_epoch_train, decimals=8),\\\n",
    "                    np.around(avg_losses_epoch_valid, decimals=8),\\\n",
    "                    np.around([cur_time - pre_time] , decimals=2),\\\n",
    "                    is_best_model) )\n",
    "        pre_time = cur_time\n",
    "    return best_model, [losses_train, losses_valid, losses_epochs_train, losses_epochs_valid]\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "def TestModel(model, test_dataloader, max_speed):\n",
    "    \n",
    "    inputs, labels = next(iter(test_dataloader))\n",
    "    [batch_size, step_size, fea_size] = inputs.size()\n",
    "\n",
    "    cur_time = time.time()\n",
    "    pre_time = time.time()\n",
    "    \n",
    "    use_gpu = torch.cuda.is_available()\n",
    "    \n",
    "    loss_MSE = torch.nn.MSELoss()\n",
    "    loss_L1 = torch.nn.MSELoss()\n",
    "    \n",
    "    tested_batch = 0\n",
    "    \n",
    "    losses_mse = []\n",
    "    losses_l1 = [] \n",
    "    \n",
    "    for data in test_dataloader:\n",
    "        inputs, labels = data\n",
    "        \n",
    "        if inputs.shape[0] != batch_size:\n",
    "            continue\n",
    "    \n",
    "        if use_gpu:\n",
    "            inputs, labels = Variable(inputs.cuda()), Variable(labels.cuda())\n",
    "        else: \n",
    "            inputs, labels = Variable(inputs), Variable(labels)\n",
    "\n",
    "        # rnn.loop() \n",
    "        hidden = model.initHidden(batch_size)\n",
    "\n",
    "        outputs = None\n",
    "        outputs = model(inputs)\n",
    "    \n",
    "    \n",
    "        loss_MSE = torch.nn.MSELoss()\n",
    "        loss_L1 = torch.nn.L1Loss()\n",
    "        loss_mse = loss_MSE(outputs, torch.squeeze(labels))\n",
    "        loss_l1 = loss_L1(outputs, torch.squeeze(labels))\n",
    "    \n",
    "        losses_mse.append(loss_mse.cpu().data.numpy())\n",
    "        losses_l1.append(loss_l1.cpu().data.numpy())\n",
    "    \n",
    "        tested_batch += 1\n",
    "    \n",
    "        if tested_batch % 1000 == 0:\n",
    "            cur_time = time.time()\n",
    "            print('Tested #: {}, loss_l1: {}, loss_mse: {}, time: {}'.format( \\\n",
    "                  tested_batch * batch_size, \\\n",
    "                  np.around([loss_l1.data[0]], decimals=8), \\\n",
    "                  np.around([loss_mse.data[0]], decimals=8), \\\n",
    "                  np.around([cur_time - pre_time], decimals=8) ) )\n",
    "            pre_time = cur_time\n",
    "    losses_l1 = np.array(losses_l1)\n",
    "    losses_mse = np.array(losses_mse)\n",
    "    mean_l1 = np.mean(losses_l1) * max_speed\n",
    "    std_l1 = np.std(losses_l1) * max_speed\n",
    "    \n",
    "    print('Tested: L1_mean: {}, L1_std : {}'.format(mean_l1, std_l1))\n",
    "    return [losses_l1, losses_mse, mean_l1, std_l1]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "class LSTM(nn.Module):\n",
    "    def __init__(self, input_size, cell_size, hidden_size, output_last = True):\n",
    "        \"\"\"\n",
    "        cell_size is the size of cell_state.\n",
    "        hidden_size is the size of hidden_state, or say the output_state of each step\n",
    "        \"\"\"\n",
    "        super(LSTM, self).__init__()\n",
    "        \n",
    "        self.cell_size = cell_size\n",
    "        self.hidden_size = hidden_size\n",
    "        self.fl = nn.Linear(input_size + hidden_size, hidden_size)\n",
    "        self.il = nn.Linear(input_size + hidden_size, hidden_size)\n",
    "        self.ol = nn.Linear(input_size + hidden_size, hidden_size)\n",
    "        self.Cl = nn.Linear(input_size + hidden_size, hidden_size)\n",
    "        \n",
    "        self.output_last = output_last\n",
    "        \n",
    "    def step(self, input, Hidden_State, Cell_State):\n",
    "        combined = torch.cat((input, Hidden_State), 1)\n",
    "        f = F.sigmoid(self.fl(combined))\n",
    "        i = F.sigmoid(self.il(combined))\n",
    "        o = F.sigmoid(self.ol(combined))\n",
    "        C = F.tanh(self.Cl(combined))\n",
    "        Cell_State = f * Cell_State + i * C\n",
    "        Hidden_State = o * F.tanh(Cell_State)\n",
    "        \n",
    "        return Hidden_State, Cell_State\n",
    "    \n",
    "    def forward(self, inputs):\n",
    "        batch_size = inputs.size(0)\n",
    "        time_step = inputs.size(1)\n",
    "        Hidden_State, Cell_State = self.initHidden(batch_size)\n",
    "        \n",
    "        if self.output_last:\n",
    "            for i in range(time_step):\n",
    "                Hidden_State, Cell_State = self.step(torch.squeeze(inputs[:,i:i+1,:]), Hidden_State, Cell_State)  \n",
    "            return Hidden_State\n",
    "        else:\n",
    "            outputs = None\n",
    "            for i in range(time_step):\n",
    "                Hidden_State, Cell_State = self.step(torch.squeeze(inputs[:,i:i+1,:]), Hidden_State, Cell_State)  \n",
    "                if outputs is None:\n",
    "                    outputs = Hidden_State.unsqueeze(1)\n",
    "                else:\n",
    "                    outputs = torch.cat((outputs, Hidden_State.unsqueeze(1)), 1)\n",
    "            return outputs\n",
    "    \n",
    "    def initHidden(self, batch_size):\n",
    "        use_gpu = torch.cuda.is_available()\n",
    "        if use_gpu:\n",
    "            Hidden_State = Variable(torch.zeros(batch_size, self.hidden_size).cuda())\n",
    "            Cell_State = Variable(torch.zeros(batch_size, self.hidden_size).cuda())\n",
    "            return Hidden_State, Cell_State\n",
    "        else:\n",
    "            Hidden_State = Variable(torch.zeros(batch_size, self.hidden_size))\n",
    "            Cell_State = Variable(torch.zeros(batch_size, self.hidden_size))\n",
    "            return Hidden_State, Cell_State\n",
    "        "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [],
   "source": [
    "class ConvLSTM(nn.Module):\n",
    "    def __init__(self, input_size, cell_size, hidden_size, output_last = True):\n",
    "        \"\"\"\n",
    "        cell_size is the size of cell_state.\n",
    "        hidden_size is the size of hidden_state, or say the output_state of each step\n",
    "        \"\"\"\n",
    "        super(ConvLSTM, self).__init__()\n",
    "        \n",
    "        self.cell_size = cell_size\n",
    "        self.hidden_size = hidden_size\n",
    "        self.fl = nn.Linear(input_size + hidden_size, hidden_size)\n",
    "        self.il = nn.Linear(input_size + hidden_size, hidden_size)\n",
    "        self.ol = nn.Linear(input_size + hidden_size, hidden_size)\n",
    "        self.Cl = nn.Linear(input_size + hidden_size, hidden_size)\n",
    "        \n",
    "        self.conv = nn.Conv1d(1, hidden_size, hidden_size)\n",
    "        \n",
    "        self.output_last = output_last\n",
    "        \n",
    "    def step(self, input, Hidden_State, Cell_State):\n",
    "        \n",
    "        conv = self.conv(input)\n",
    "        \n",
    "        combined = torch.cat((conv, Hidden_State), 1)\n",
    "        f = F.sigmoid(self.fl(combined))\n",
    "        i = F.sigmoid(self.il(combined))\n",
    "        o = F.sigmoid(self.ol(combined))\n",
    "        C = F.tanh(self.Cl(combined))\n",
    "        Cell_State = f * Cell_State + i * C\n",
    "        Hidden_State = o * F.tanh(Cell_State)\n",
    "        \n",
    "        return Hidden_State, Cell_State\n",
    "    \n",
    "    def forward(self, inputs):\n",
    "        batch_size = inputs.size(0)\n",
    "        time_step = inputs.size(1)\n",
    "        Hidden_State, Cell_State = self.initHidden(batch_size)\n",
    "        \n",
    "        if self.output_last:\n",
    "            for i in range(time_step):\n",
    "                Hidden_State, Cell_State = self.step(torch.squeeze(inputs[:,i:i+1,:]), Hidden_State, Cell_State)  \n",
    "            return Hidden_State\n",
    "        else:\n",
    "            outputs = None\n",
    "            for i in range(time_step):\n",
    "                Hidden_State, Cell_State = self.step(torch.squeeze(inputs[:,i:i+1,:]), Hidden_State, Cell_State)  \n",
    "                if outputs is None:\n",
    "                    outputs = Hidden_State.unsqueeze(1)\n",
    "                else:\n",
    "                    outputs = torch.cat((outputs, Hidden_State.unsqueeze(1)), 1)\n",
    "            return outputs\n",
    "    \n",
    "    def initHidden(self, batch_size):\n",
    "        use_gpu = torch.cuda.is_available()\n",
    "        if use_gpu:\n",
    "            Hidden_State = Variable(torch.zeros(batch_size, self.hidden_size).cuda())\n",
    "            Cell_State = Variable(torch.zeros(batch_size, self.hidden_size).cuda())\n",
    "            return Hidden_State, Cell_State\n",
    "        else:\n",
    "            Hidden_State = Variable(torch.zeros(batch_size, self.hidden_size))\n",
    "            Cell_State = Variable(torch.zeros(batch_size, self.hidden_size))\n",
    "            return Hidden_State, Cell_State\n",
    "        "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "class LocalizedSpectralGraphConvolution(nn.Module):\n",
    "    def __init__(self, A, K):\n",
    "        \n",
    "        super(LocalizedSpectralGraphConvolution, self).__init__()\n",
    "        \n",
    "        \n",
    "        self.K = K\n",
    "        self.A = A.cuda()\n",
    "        feature_size = A.shape[0]\n",
    "        self.D = torch.diag(torch.sum(self.A, dim=0)).cuda()\n",
    "        \n",
    "        I = torch.eye(feature_size,feature_size).cuda()\n",
    "        self.L = I - torch.inverse(torch.sqrt(self.D)).matmul(self.A).matmul(torch.inverse(torch.sqrt(self.D))) \n",
    "        \n",
    "        L_temp = I\n",
    "        for i in range(K):\n",
    "            L_temp = torch.matmul(L_temp, self.L)\n",
    "            if i == 0:\n",
    "                self.L_tensor = torch.unsqueeze(L_temp, 2)\n",
    "            else:\n",
    "                self.L_tensor = torch.cat((self.L_tensor, torch.unsqueeze(L_temp, 2)), 2)\n",
    "            \n",
    "        self.L_tensor = Variable(self.L_tensor.cuda(), requires_grad=False)\n",
    "\n",
    "        self.params = Parameter(torch.FloatTensor(K).cuda())\n",
    "        \n",
    "        stdv = 1. / math.sqrt(K)\n",
    "        for i in range(K):\n",
    "            self.params[i].data.uniform_(-stdv, stdv)\n",
    "\n",
    "    def forward(self, input):\n",
    "        x = input\n",
    "\n",
    "        conv = x.matmul( torch.sum(self.params.expand_as(self.L_tensor) * self.L_tensor, 2) )\n",
    "\n",
    "        return conv\n",
    "        \n",
    "        \n",
    "class LocalizedSpectralGraphConvolutionalLSTM(nn.Module):\n",
    "    \n",
    "    def __init__(self, K, A, feature_size, Clamp_A=True, output_last = True):\n",
    "        '''\n",
    "        Args:\n",
    "            K: K-hop graph\n",
    "            A: adjacency matrix\n",
    "            FFR: free-flow reachability matrix\n",
    "            feature_size: the dimension of features\n",
    "            Clamp_A: Boolean value, clamping all elements of A between 0. to 1.\n",
    "        '''\n",
    "        super(LocalizedSpectralGraphConvolutionalLSTM, self).__init__()\n",
    "        self.feature_size = feature_size\n",
    "        self.hidden_size = feature_size\n",
    "        \n",
    "        self.K = K\n",
    "        self.A = A\n",
    "        self.gconv = LocalizedSpectralGraphConvolution(A, K)\n",
    "    \n",
    "        hidden_size = self.feature_size\n",
    "        input_size = self.feature_size + hidden_size\n",
    "\n",
    "        self.fl = nn.Linear(input_size, hidden_size)\n",
    "        self.il = nn.Linear(input_size, hidden_size)\n",
    "        self.ol = nn.Linear(input_size, hidden_size)\n",
    "        self.Cl = nn.Linear(input_size, hidden_size)\n",
    "        \n",
    "        self.output_last = output_last\n",
    "        \n",
    "    def step(self, input, Hidden_State, Cell_State):\n",
    "        \n",
    "#         conv_sample_start = time.time()  \n",
    "        conv = F.relu(self.gconv(input))\n",
    "#         conv_sample_end = time.time()  \n",
    "#         print('conv_sample:', (conv_sample_end - conv_sample_start))\n",
    "        combined = torch.cat((conv, Hidden_State), 1)\n",
    "        f = F.sigmoid(self.fl(combined))\n",
    "        i = F.sigmoid(self.il(combined))\n",
    "        o = F.sigmoid(self.ol(combined))\n",
    "        C = F.tanh(self.Cl(combined))\n",
    "        Cell_State = f * Cell_State + i * C\n",
    "        Hidden_State = o * F.tanh(Cell_State)\n",
    "        \n",
    "        return Hidden_State, Cell_State\n",
    "    \n",
    "    def Bi_torch(self, a):\n",
    "        a[a < 0] = 0\n",
    "        a[a > 0] = 1\n",
    "        return a\n",
    "    \n",
    "    def forward(self, inputs):\n",
    "        batch_size = inputs.size(0)\n",
    "        time_step = inputs.size(1)\n",
    "        Hidden_State, Cell_State = self.initHidden(batch_size)\n",
    "        \n",
    "        outputs = None\n",
    "        \n",
    "        for i in range(time_step):\n",
    "            Hidden_State, Cell_State = self.step(torch.squeeze(inputs[:,i:i+1,:]), Hidden_State, Cell_State)  \n",
    "\n",
    "            if outputs is None:\n",
    "                outputs = Hidden_State.unsqueeze(1)\n",
    "            else:\n",
    "                outputs = torch.cat((outputs, Hidden_State.unsqueeze(1)), 1)\n",
    "#         print(type(outputs))\n",
    "        \n",
    "        if self.output_last:\n",
    "            return outputs[:,-1,:]\n",
    "        else:\n",
    "            return outputs\n",
    "    \n",
    "    def initHidden(self, batch_size):\n",
    "        use_gpu = torch.cuda.is_available()\n",
    "        if use_gpu:\n",
    "            Hidden_State = Variable(torch.zeros(batch_size, self.hidden_size).cuda())\n",
    "            Cell_State = Variable(torch.zeros(batch_size, self.hidden_size).cuda())\n",
    "            return Hidden_State, Cell_State\n",
    "        else:\n",
    "            Hidden_State = Variable(torch.zeros(batch_size, self.hidden_size))\n",
    "            Cell_State = Variable(torch.zeros(batch_size, self.hidden_size))\n",
    "            return Hidden_State, Cell_State\n",
    "    def reinitHidden(self, batch_size, Hidden_State_data, Cell_State_data):\n",
    "        use_gpu = torch.cuda.is_available()\n",
    "        if use_gpu:\n",
    "            Hidden_State = Variable(Hidden_State_data.cuda(), requires_grad=True)\n",
    "            Cell_State = Variable(Cell_State_data.cuda(), requires_grad=True)\n",
    "            return Hidden_State, Cell_State\n",
    "        else:\n",
    "            Hidden_State = Variable(Hidden_State_data, requires_grad=True)\n",
    "            Cell_State = Variable(Cell_State_data, requires_grad=True)\n",
    "            return Hidden_State, Cell_State"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "class SpectralGraphConvolution(nn.Module):\n",
    "    def __init__(self, A):\n",
    "        \n",
    "        super(SpectralGraphConvolution, self).__init__()\n",
    "        \n",
    "        feature_size = A.shape[0]\n",
    "        \n",
    "        self.A = A\n",
    "        self.D = torch.diag(torch.sum(self.A, dim=0))\n",
    "        self.L = D - A\n",
    "        self.param = Parameter(torch.FloatTensor(feature_size).cuda())\n",
    "        stdv = 1. / math.sqrt(feature_size)\n",
    "        self.param.data.uniform_(-stdv, stdv)\n",
    "        \n",
    "        self.e, self.v = torch.eig(L_, eigenvectors=True)\n",
    "        self.vt = torch.t(self.v)\n",
    "        self.v = Variable(self.v.cuda(), requires_grad=False)\n",
    "        self.vt = Variable(self.vt.cuda(), requires_grad=False)\n",
    "    \n",
    "    def forward(self, input):\n",
    "        x = input\n",
    "        conv_sample_start = time.time()  \n",
    "        conv = x.matmul(self.v.matmul(torch.diag(self.param)).matmul(self.vt))\n",
    "        conv_sample_end = time.time()  \n",
    "        print('conv_sample:', (conv_sample_end - conv_sample_start))\n",
    "        return conv\n",
    "        \n",
    "class SpectralGraphConvolutionalLSTM(nn.Module):\n",
    "    \n",
    "    def __init__(self, K, A, feature_size, Clamp_A=True, output_last = True):\n",
    "        '''\n",
    "        Args:\n",
    "            K: K-hop graph\n",
    "            A: adjacency matrix\n",
    "            FFR: free-flow reachability matrix\n",
    "            feature_size: the dimension of features\n",
    "            Clamp_A: Boolean value, clamping all elements of A between 0. to 1.\n",
    "        '''\n",
    "        super(SpectralGraphConvolutionalLSTM, self).__init__()\n",
    "        self.feature_size = feature_size\n",
    "        self.hidden_size = feature_size\n",
    "        \n",
    "        self.K = K\n",
    "        self.A = A\n",
    "        self.gconv = SpectralGraphConvolution(A)\n",
    "    \n",
    "        hidden_size = self.feature_size\n",
    "        input_size = self.feature_size + hidden_size\n",
    "\n",
    "        self.fl = nn.Linear(input_size, hidden_size)\n",
    "        self.il = nn.Linear(input_size, hidden_size)\n",
    "        self.ol = nn.Linear(input_size, hidden_size)\n",
    "        self.Cl = nn.Linear(input_size, hidden_size)\n",
    "        \n",
    "        self.output_last = output_last\n",
    "        \n",
    "    def step(self, input, Hidden_State, Cell_State):\n",
    "        conv_sample_start = time.time()  \n",
    "        conv = self.gconv(input)\n",
    "        conv_sample_end = time.time()  \n",
    "        print('conv_sample:', (conv_sample_end - conv_sample_start))\n",
    "        combined = torch.cat((conv, Hidden_State), 1)\n",
    "        f = F.sigmoid(self.fl(combined))\n",
    "        i = F.sigmoid(self.il(combined))\n",
    "        o = F.sigmoid(self.ol(combined))\n",
    "        C = F.tanh(self.Cl(combined))\n",
    "        Cell_State = f * Cell_State + i * C\n",
    "        Hidden_State = o * F.tanh(Cell_State)\n",
    "        \n",
    "        return Hidden_State, Cell_State\n",
    "    \n",
    "    def Bi_torch(self, a):\n",
    "        a[a < 0] = 0\n",
    "        a[a > 0] = 1\n",
    "        return a\n",
    "    \n",
    "    def forward(self, inputs):\n",
    "        batch_size = inputs.size(0)\n",
    "        time_step = inputs.size(1)\n",
    "        Hidden_State, Cell_State = self.initHidden(batch_size)\n",
    "        \n",
    "        outputs = None\n",
    "        \n",
    "        train_sample_start = time.time()  \n",
    "        \n",
    "        for i in range(time_step):\n",
    "            Hidden_State, Cell_State = self.step(torch.squeeze(inputs[:,i:i+1,:]), Hidden_State, Cell_State)  \n",
    "\n",
    "            if outputs is None:\n",
    "                outputs = Hidden_State.unsqueeze(1)\n",
    "            else:\n",
    "                outputs = torch.cat((outputs, Hidden_State.unsqueeze(1)), 1)\n",
    "        \n",
    "        train_sample_end = time.time()\n",
    "        print('train sample:' , (train_sample_end - train_sample_start))\n",
    "        if self.output_last:\n",
    "            return outputs[:,-1,:]\n",
    "        else:\n",
    "            return outputs\n",
    "    \n",
    "    def initHidden(self, batch_size):\n",
    "        use_gpu = torch.cuda.is_available()\n",
    "        if use_gpu:\n",
    "            Hidden_State = Variable(torch.zeros(batch_size, self.hidden_size).cuda())\n",
    "            Cell_State = Variable(torch.zeros(batch_size, self.hidden_size).cuda())\n",
    "            return Hidden_State, Cell_State\n",
    "        else:\n",
    "            Hidden_State = Variable(torch.zeros(batch_size, self.hidden_size))\n",
    "            Cell_State = Variable(torch.zeros(batch_size, self.hidden_size))\n",
    "            return Hidden_State, Cell_State\n",
    "    def reinitHidden(self, batch_size, Hidden_State_data, Cell_State_data):\n",
    "        use_gpu = torch.cuda.is_available()\n",
    "        if use_gpu:\n",
    "            Hidden_State = Variable(Hidden_State_data.cuda(), requires_grad=True)\n",
    "            Cell_State = Variable(Cell_State_data.cuda(), requires_grad=True)\n",
    "            return Hidden_State, Cell_State\n",
    "        else:\n",
    "            Hidden_State = Variable(Hidden_State_data, requires_grad=True)\n",
    "            Cell_State = Variable(Cell_State_data, requires_grad=True)\n",
    "            return Hidden_State, Cell_State"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "class GraphConvolutionalLSTM(nn.Module):\n",
    "    \n",
    "    def __init__(self, K, A, FFR, feature_size, Clamp_A=True, output_last = True):\n",
    "        '''\n",
    "        Args:\n",
    "            K: K-hop graph\n",
    "            A: adjacency matrix\n",
    "            FFR: free-flow reachability matrix\n",
    "            feature_size: the dimension of features\n",
    "            Clamp_A: Boolean value, clamping all elements of A between 0. to 1.\n",
    "        '''\n",
    "        super(GraphConvolutionalLSTM, self).__init__()\n",
    "        self.feature_size = feature_size\n",
    "        self.hidden_size = feature_size\n",
    "        \n",
    "        self.K = K\n",
    "        \n",
    "        self.A_list = [] # Adjacency Matrix List\n",
    "        A = torch.FloatTensor(A)\n",
    "        A_temp = torch.eye(feature_size,feature_size)\n",
    "        for i in range(K):\n",
    "            A_temp = torch.matmul(A_temp, torch.Tensor(A))\n",
    "            if Clamp_A:\n",
    "                # confine elements of A \n",
    "                A_temp = torch.clamp(A_temp, max = 1.) \n",
    "            self.A_list.append(torch.mul(A_temp, torch.Tensor(FFR)))\n",
    "#             self.A_list.append(A_temp)\n",
    "        \n",
    "        # a length adjustable Module List for hosting all graph convolutions\n",
    "        self.gc_list = nn.ModuleList([FilterLinear(feature_size, feature_size, self.A_list[i], bias=False) for i in range(K)])                  \n",
    "        \n",
    "        hidden_size = self.feature_size\n",
    "        input_size = self.feature_size * K\n",
    "\n",
    "        self.fl = nn.Linear(input_size + hidden_size, hidden_size)\n",
    "        self.il = nn.Linear(input_size + hidden_size, hidden_size)\n",
    "        self.ol = nn.Linear(input_size + hidden_size, hidden_size)\n",
    "        self.Cl = nn.Linear(input_size + hidden_size, hidden_size)\n",
    "        \n",
    "        # initialize the neighbor weight for the cell state\n",
    "        self.Neighbor_weight = Parameter(torch.FloatTensor(feature_size))\n",
    "        stdv = 1. / math.sqrt(feature_size)\n",
    "        self.Neighbor_weight.data.uniform_(-stdv, stdv)\n",
    "        \n",
    "        self.output_last = output_last\n",
    "        \n",
    "    def step(self, input, Hidden_State, Cell_State):\n",
    "        \n",
    "        x = input\n",
    "\n",
    "        gc = self.gc_list[0](x)\n",
    "        for i in range(1, self.K):\n",
    "            gc = torch.cat((gc, self.gc_list[i](x)), 1)\n",
    "            \n",
    "        combined = torch.cat((gc, Hidden_State), 1)\n",
    "        f = F.sigmoid(self.fl(combined))\n",
    "        i = F.sigmoid(self.il(combined))\n",
    "        o = F.sigmoid(self.ol(combined))\n",
    "        C = F.tanh(self.Cl(combined))\n",
    "\n",
    "        NC = torch.mul(Cell_State,  torch.mv(Variable(self.A_list[-1], requires_grad=False).cuda(), self.Neighbor_weight))\n",
    "        Cell_State = f * NC + i * C\n",
    "        Hidden_State = o * F.tanh(Cell_State)\n",
    "\n",
    "        return Hidden_State, Cell_State, gc\n",
    "    \n",
    "    def Bi_torch(self, a):\n",
    "        a[a < 0] = 0\n",
    "        a[a > 0] = 1\n",
    "        return a\n",
    "    \n",
    "    def forward(self, inputs):\n",
    "        batch_size = inputs.size(0)\n",
    "        time_step = inputs.size(1)\n",
    "        Hidden_State, Cell_State = self.initHidden(batch_size)\n",
    "        \n",
    "        outputs = None\n",
    "        \n",
    "        for i in range(time_step):\n",
    "            Hidden_State, Cell_State, gc = self.step(torch.squeeze(inputs[:,i:i+1,:]), Hidden_State, Cell_State)  \n",
    "\n",
    "            if outputs is None:\n",
    "                outputs = Hidden_State.unsqueeze(1)\n",
    "            else:\n",
    "                outputs = torch.cat((outputs, Hidden_State.unsqueeze(1)), 1)\n",
    "        \n",
    "        if self.output_last:\n",
    "            return outputs[:,-1,:]\n",
    "        else:\n",
    "            return outputs\n",
    "    \n",
    "    def initHidden(self, batch_size):\n",
    "        use_gpu = torch.cuda.is_available()\n",
    "        if use_gpu:\n",
    "            Hidden_State = Variable(torch.zeros(batch_size, self.hidden_size).cuda())\n",
    "            Cell_State = Variable(torch.zeros(batch_size, self.hidden_size).cuda())\n",
    "            return Hidden_State, Cell_State\n",
    "        else:\n",
    "            Hidden_State = Variable(torch.zeros(batch_size, self.hidden_size))\n",
    "            Cell_State = Variable(torch.zeros(batch_size, self.hidden_size))\n",
    "            return Hidden_State, Cell_State\n",
    "    def reinitHidden(self, batch_size, Hidden_State_data, Cell_State_data):\n",
    "        use_gpu = torch.cuda.is_available()\n",
    "        if use_gpu:\n",
    "            Hidden_State = Variable(Hidden_State_data.cuda(), requires_grad=True)\n",
    "            Cell_State = Variable(Cell_State_data.cuda(), requires_grad=True)\n",
    "            return Hidden_State, Cell_State\n",
    "        else:\n",
    "            Hidden_State = Variable(Hidden_State_data, requires_grad=True)\n",
    "            Cell_State = Variable(Cell_State_data, requires_grad=True)\n",
    "            return Hidden_State, Cell_State"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "lstm = LSTM(input_dim, hidden_dim, output_dim, output_last = True)\n",
    "lstm, lstm_loss = TrainModel(lstm, train_dataloader, valid_dataloader, num_epochs = 1)\n",
    "lstm_test = TestModel(lstm, test_dataloader, max_speed )"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "K = 64\n",
    "Clamp_A = False\n",
    "lsgclstm = LocalizedSpectralGraphConvolutionalLSTM(K, torch.Tensor(A), A.shape[0], Clamp_A=Clamp_A, output_last = True)\n",
    "lsgclstm, lsgclstm_loss = TrainModel(lsgclstm, train_dataloader, valid_dataloader, num_epochs = 1)\n",
    "lsgclstm_test = TestModel(lsgclstm, test_dataloader, max_speed )"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "K = 3\n",
    "back_length = 3\n",
    "Clamp_A = False\n",
    "sgclstm = SpectralGraphConvolutionalLSTM(K, torch.Tensor(A), A.shape[0], Clamp_A=Clamp_A, output_last = True)\n",
    "sgclstm, sgclstm_loss = TrainModel(sgclstm, train_dataloader, valid_dataloader, num_epochs = 1)\n",
    "sgclstm_test = TestModel(sgclstm, test_dataloader, max_speed )"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "K = 3\n",
    "back_length = 3\n",
    "Clamp_A = False\n",
    "gclstm = GraphConvolutionalLSTM(K, torch.Tensor(A), FFR[back_length], A.shape[0], Clamp_A=Clamp_A, output_last = True)\n",
    "gclstm, gclstm_loss = TrainModel(gclstm, train_dataloader, valid_dataloader, num_epochs = 1)\n",
    "gclstm_test = TestModel(gclstm, test_dataloader, max_speed )"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "rnn_val_loss = np.asarray(rnn_loss[3])\n",
    "lstm_val_loss = np.asarray(lstm_loss[3])\n",
    "hgclstm_val_loss = np.asarray(gclstm_loss[3])\n",
    "lsgclstm_val_loss = np.asarray(lsgclstm_loss[3])\n",
    "sgclstm_val_loss = np.asarray(sgclstm_loss[3])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "lstm_val_loss = np.load('lstm_val_loss.npy')\n",
    "hgclstm_val_loss = np.load('hgclstm_val_loss.npy')\n",
    "lsgclstm_val_loss = np.load('lsgclstm_val_loss.npy')\n",
    "sgclstm_val_loss = np.load('sgclstm_val_loss.npy')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "# np.save('lstm_val_loss', lstm_val_loss)\n",
    "# np.save('hgclstm_val_loss', gclstm_val_loss)\n",
    "# np.save('lsgclstm_val_loss', lsgclstm_val_loss)\n",
    "# np.save('sgclstm_val_loss', sgclstm_val_loss)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "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\n",
      "text/plain": [
       "<Figure size 432x288 with 1 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    }
   ],
   "source": [
    "fig, ax = plt.subplots()\n",
    "plt.plot(np.arange(1, len(lstm_val_loss) + 1), lstm_val_loss,  label = 'LSTM')\n",
    "plt.plot(np.arange(1, len(sgclstm_val_loss) + 1), sgclstm_val_loss,  label = 'SGC+LSTM')\n",
    "plt.plot(np.arange(1, len(lsgclstm_val_loss) + 1), lsgclstm_val_loss,  label = 'LSGC+LSTM')\n",
    "plt.plot(np.arange(1, len(hgclstm_val_loss) + 1),hgclstm_val_loss,  label = 'HGC-LSTM')\n",
    "plt.ylim((6 * 0.0001, 0.0019))\n",
    "plt.xticks(fontsize = 12)\n",
    "plt.yticks(fontsize = 12)\n",
    "plt.yscale('log')\n",
    "plt.ylabel('Validation Loss (MSE)', fontsize=12)\n",
    "plt.xlabel('Epoch', fontsize=12)\n",
    "# plt.gca().invert_xaxis()\n",
    "plt.legend(fontsize=14)\n",
    "plt.grid(True, which='both')\n",
    "\n",
    "plt.savefig('Validation_loss.png', dpi=300, bbox_inches = 'tight', pad_inches=0.1)"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.7.0"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
